package com.kingwang.study.spring.security.demo.security.provider;

import com.kingwang.study.spring.security.demo.security.token.TokenBasedAuthenticationRequestToken;
import com.kingwang.study.spring.security.demo.security.token.TokenBasedAuthenticationResultToken;
import com.kingwang.study.spring.security.demo.security.token.session.TokenSession;
import com.kingwang.study.spring.security.demo.security.token.session.TokenSessionManager;
import lombok.Getter;
import lombok.Setter;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.SpringSecurityMessageSource;
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
import org.springframework.security.core.authority.mapping.NullAuthoritiesMapper;
import org.springframework.security.core.userdetails.UserCache;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.core.userdetails.cache.NullUserCache;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.util.Assert;

/**
 * Token认证的AuthenticationProvider
 */
public class TokenBasedAuthenticationProvider implements AuthenticationProvider {
    protected final Log logger = LogFactory.getLog(getClass());

    /**
     * 强制返回的Authentication对象的Principal属性为String
     */
    @Setter
    private boolean forcePrincipalAsString = false;

    /**
     * Spring Security统一消息源
     */
    @Setter
    protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();

    /**
     * 用户信息缓存
     * 可以使用内存或Redis进行扩展
     */
    @Setter
    private UserCache userCache = new NullUserCache();

    /**
     * 权限映射器
     */
    @Setter
    private GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper();

    /**
     * 用户信息获取服务，必须从外部设置
     */
    @Getter
    @Setter
    private UserDetailsService userDetailsService;

    /**
     * 密码加密器，必须从外部设置
     */
    @Getter
    private PasswordEncoder passwordEncoder;

    /**
     * Token会话管理器，必须从外部设置
     */
    @Setter
    private TokenSessionManager tokenSessionManager;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        // 判断是否为本Provider支持的Authentication类型
        Assert.isInstanceOf(TokenBasedAuthenticationRequestToken.class, authentication,
                () -> this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports",
                        "Only TokenBasedAuthenticationRequestToken is supported"));

        // 获取用户名
        String username = authentication.getName();

        // 先从用户信息缓存中获取用户信息
        boolean cacheWasUsed = true;
        UserDetails user = this.userCache.getUserFromCache(username);

        if (user == null) {
            // 如果缓存中不存在该用户，使用UserDetailsService获取
            cacheWasUsed = false;

            try {
                user = retrieveUser(username, (TokenBasedAuthenticationRequestToken) authentication);
            } catch (UsernameNotFoundException ex) {
                this.logger.debug("Failed to find user '" + username + "'");

                throw new BadCredentialsException(this.messages
                        .getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
            }

            Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
        }

        // 验证密码是否符合
        try {
            additionalAuthenticationChecks(user, (TokenBasedAuthenticationRequestToken) authentication);
        } catch (AuthenticationException ex) {
            // 如果出现异常（认证失败），如果未使用用户信息缓存，则抛出该异常（认证失败）
            if (!cacheWasUsed) {
                throw ex;
            }

            // 如果使用了用户信息缓存，可能是缓存中的信息未及时更新，所以需要再从UserDetailsService中再获取最新的数据进行比较
            // There was a problem, so try again after checking
            // we're using latest data (i.e. not from the cache)
            cacheWasUsed = false;
            user = retrieveUser(username, (TokenBasedAuthenticationRequestToken) authentication);

            additionalAuthenticationChecks(user, (TokenBasedAuthenticationRequestToken) authentication);
        }

        // 将用户信息加入缓存
        if (!cacheWasUsed) {
            this.userCache.putUserInCache(user);
        }

        // 如果设置了Principal强制为String，则Principal数据为用户信息中的username属性
        Object principalToReturn = user;
        if (this.forcePrincipalAsString) {
            principalToReturn = user.getUsername();
        }

        // 创建成功认证的Authentication对象
        return createSuccessAuthentication(principalToReturn, authentication, user);
    }

    public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
        Assert.notNull(passwordEncoder, "passwordEncoder cannot be null");
        this.passwordEncoder = passwordEncoder;
    }

    /**
     * 验证密码等
     *
     * @param userDetails
     * @param authentication
     */
    protected void additionalAuthenticationChecks(UserDetails userDetails, TokenBasedAuthenticationRequestToken authentication) {
        if (authentication.getCredentials() == null) {
            this.logger.debug("Failed to authenticate since no credentials provided");
            throw new BadCredentialsException(this.messages
                    .getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
        }

        String presentedPassword = authentication.getCredentials().toString();
        if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
            this.logger.debug("Failed to authenticate since password does not match stored value");
            throw new BadCredentialsException(this.messages
                    .getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
        }
    }

    /**
     * 检查是否为本Provider支持的Authentication对象
     *
     * @param authentication
     * @return
     */
    @Override
    public boolean supports(Class<?> authentication) {
        return TokenBasedAuthenticationRequestToken.class.isAssignableFrom(authentication);
    }

    /**
     * 从用户信息获取服务中获取用户信息
     *
     * @param username
     * @param authentication
     * @return
     * @throws AuthenticationException
     */
    protected UserDetails retrieveUser(String username, TokenBasedAuthenticationRequestToken authentication)
            throws AuthenticationException {
        try {
            UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
            if (loadedUser == null) {
                throw new InternalAuthenticationServiceException(
                        "UserDetailsService returned null, which is an interface contract violation");
            }
            return loadedUser;
        } catch (UsernameNotFoundException ex) {
            throw ex;
        } catch (InternalAuthenticationServiceException ex) {
            throw ex;
        } catch (Exception ex) {
            throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
        }
    }

    /**
     * 创建认证成功的Authentication凭据
     *
     * @param principal
     * @param authentication
     * @param user
     * @return
     */
    protected Authentication createSuccessAuthentication(Object principal, Authentication authentication,
                                                         UserDetails user) {
        // 生成并存储Token
        TokenSession token = this.tokenSessionManager.createSession("Bearer", user);

        // 返回认证成功的Authentication对象，注意此时使用的对象类型与刚进入认证时不同
        TokenBasedAuthenticationResultToken result = new TokenBasedAuthenticationResultToken(
                principal,
                authentication.getCredentials(),
                token.getType(),
                token.getToken(),
                this.authoritiesMapper.mapAuthorities(user.getAuthorities()));

        result.setDetails(authentication.getDetails());
        
        this.logger.debug("Authenticated user");

        return result;
    }
}
